<?php
/**
 * 上传类
 * 
 * Copyright (c) 2014~2019 https://desion.net All rights reserved.
 * Author: 阿林 QQ:120867629 <linhong00@gmail.com>
 */
declare (strict_types = 1);
namespace dsphp;

class Upload
{
    // 上传数据
    private $_data = [];
    // 图片后缀
    private $_picExt = ['gif','jpg','jpeg','bmp','png'];
    // 错误信息
    public $error = '';
    // 配置信息
    protected $config = [
        'mimes'         => [], //允许上传的文件MiMe类型
        'max_size'      => 0, //上传的文件大小限制 (0-不做限制)
        'exts'          => ['gif','jpg','jpeg','bmp','png'], //允许上传的文件后缀
        'root_path'     => 'upload',
        'replace'       => false, //存在同名是否覆盖
        'hash'          => false, //是否生成hash编码
        // 'delete'        => true // 多个删除删除已经已经删除
    ];

    protected $option = [
        'save_path'     => '', // 保存路径
        'save_name'     => false, // 
        'save_ext'      => '', //文件保存后缀，空则使用原后缀
    ];

    /**
     * 构造函数
     */
    public function __construct(string $path, array $config = [])
    {
        if ($config) {
            $this->config = array_merge($this->config, $config);
        }

        $this->config['root_path'] = $this->config['root_path'] ?: 'upload';
        $this->config['root_path'] = $path . $this->config['root_path'] . DIRECTORY_SEPARATOR;
    }

    /**
     * 设置上传数据
     *
     * @param mixed $data  上传对象
     * @return this
     */
    public function data($data)
    {
        $this->_data = $data;
        return $this;
    }

    /**
     * 配置信息
     *
     * @param string $field   key
     * @param string $value 
     * @return this
     */
    public function config(string $field, $value = '')
    {
        $this->config[$field] = $value ?: '';
        return $this;
    }

    /**
     * 配置信息
     *
     * @param string $field   key
     * @param string $value 
     * @return this
     */
    public function option(string $field, $value)
    {
        $this->option[$field] = $value;
        return $this;
    }
    // 设置保存路径
    public function setSavePath(string $value)
    {
        $this->option['save_path'] = $value;
        return $this;
    }
    // 设置名称
    public function setSaveName(string $value)
    {
        $this->option['save_name'] = $value;
        return $this;
    }
    // 设置扩展名
    public function setSaveExt(string $value)
    {
        $this->option['save_ext'] = $value;
        return $this;
    }

    // 初始化
    private function _initOption()
    {
        $option = $this->option;

        if (! is_string($option['save_name'])) {
            $time   = explode('.', microtime(true));
            $value  = date('Ymd_His', $time[0]);
            $value .= '_' . $time[1];

            $this->setSaveName($value);
        }

        if ($option['save_path'] == '') {
            $this->setSavePath(date('Ym'));
        }
    }

    /**
     * 上传文件
     */
    public function move()
    {
        $this->_initOption();
        // 检测文件
        $files = $this->_checkFiles();
        if (empty($files) || is_array($files)) {
            return false;
        }
        // 检测目录
        $result = $this->_checkPath();
        if ($result === false) {
            return false;
        }
        
        $info = [];
        foreach ($files as $key => $file) {
            // 移动文件
            if (! move_uploaded_file($file['tmp_name'], $file['move_file'])) {
                $this->error = '文件上传保存错误！';
                $info[$key]  = $this->error;
                continue;
            }

            unset($file['error'], $file['tmp_name'], $file['move_file']);
            $info[$key] = $file;
        }
        
        return empty($info) ? false : $info;
    }

    /**
     * 检测文件
     */
    private function _checkFiles()
    {
        $list = $this->_dealFiles();
        if (empty($list)) {
            return false;
        }

        if (function_exists('finfo_open')) {
            $finfo = finfo_open(FILEINFO_MIME_TYPE);
        }

        foreach ($list as $key => $file) {
            // 文件上传失败，捕获错误代码 
            if ($file['error']) {
                $this->_fileError($file['error']);
                return false;
            }
            // 文件名
            $file['name'] = strip_tags($file['name']);
            if (empty($file['name'])) {
                $this->error = '未知上传错误！';
                return false;
            }
            // key
            if (! isset($file['key'])) {
                $file['key'] = $key;
            }
            // 检查是否合法上传
            if (! is_uploaded_file($file['tmp_name'])) {
                $this->error = '非法上传文件！';
                return false;
            }
            // 检查文件大小
            if ($this->config['max_size'] && $file['size'] > $this->config['max_size']) {
                $this->error = '上传文件大小不符！';
                return false;
            }
            // FLASH上传的文件获取到的mime类型都为application/octet-stream
            if (isset($finfo)) {
                $file['type'] = finfo_file($finfo, $file['tmp_name']);
            }
            // MIME 
            if ($this->config['mimes'] && in_array(strtolower($file['type']), $this->config['mimes'])) {
                $this->error = '上传文件MIME类型不允许！';
                return false;
            }
            // 扩展名称
            $file['ext'] = $this->_checkExt($file);
            if ($file['ext'] == '') {
                return false;
            }
            // 获取文件hash
            if ($this->config['hash']) {
                $file['md5']  = md5_file($file['tmp_name']);
                $file['sha1'] = sha1_file($file['tmp_name']);
            }
            // 生成保存文件名
            $file['savename'] = $this->_getSaveName($file);
            if (empty($file['savename'])) {
                return false;
            }

            $file['move_file'] = $this->config['root_path'] . $this->config['save_path'] . DIRECTORY_SEPARATOR . $file['savename'];
            // 不覆盖同名文件
            if (! $this->config['replace'] && is_file($file['move_file'])) {
                $this->error = '存在同名文件' . $file['savename'];
                return false;
            }

            $list[$key] = $file;
        }

        if (isset($finfo)) {
            finfo_close($finfo);
        }

        return $list;
    }

    /**
     * 转换上传文件数组变量为正确的方式
     */
    private function _dealFiles():array
    {
        $list = [];
        $data = $this->_data;

        if (empty($data)) {
            $this->error = '无上传数据';
            return $list;
        }

        $n = 0;

        foreach ($data as $key => $file)
        {
            if (is_array($file['name'])) {
                $keys  = array_keys($file);
                $count = count($file['name']);

                for ($i = 0; $i < $count; $i++) {
                    $list[$n]['key'] = $key;
                    foreach ($keys as $_key) {
                        $list[$n][$_key] = $file[$_key][$i];
                    }

                    $n++;
                }
            } else {
               $list = $data;
               break;
            }
        }

        return $list;
    }

    /**
     * 检测扩展名
     *
     * @param array $file
     */
    private function _checkExt($file):string
    {
        $ext = pathinfo($file['name'], PATHINFO_EXTENSION);
        if ($this->config['exts'] && in_array(strtolower($ext), $this->config['exts'])) {
            $this->error = '上传文件后缀不允许';
            return '';
        }

        // 对图像文件进行严格检测
        $ext = strtolower($ext);
        if (in_array($ext, $this->_picExt)) {
            $imginfo = getimagesize($file['tmp_name']);
            if (empty($imginfo) || ($ext == 'gif' && empty($imginfo['bits']))) {
                $this->error = '非法图像文件！';
                return '';
            }
        }

        return $ext;
    }

    /**
     * 保存名称
     *
     * @param array $file 上传文件数据
     * @return string
     */
    private function _getSaveName($file):string
    {
        $name = $this->option['save_name'];
        if (! is_string($name)) {
            $this->error = '生成保存文件名称出错';
            return '';
        }

        // 保持文件名不变
        if ($name == '') { 
            /* 解决pathinfo中文文件名BUG */
            $name = substr(pathinfo("_{$file['name']}", PATHINFO_FILENAME), 1);
        }

        return $name . '.' . ($this->option['save_ext'] ?: $file['ext']);
    }

    /**
     * 获取错误代码信息
     * @param string $errorNo  错误号
     */
    private function _fileError($errorNo)
    {
        switch ($errorNo) {
            case 1:
                $this->error = '上传的文件超过了 php.ini 中 upload_max_filesize 选项限制的值！';
                break;
            case 2:
                $this->error = '上传文件的大小超过了 HTML 表单中 MAX_FILE_SIZE 选项指定的值！';
                break;
            case 3:
                $this->error = '文件只有部分被上传！';
                break;
            case 4:
                $this->error = '没有文件被上传！';
                break;
            case 6:
                $this->error = '找不到临时文件夹！';
                break;
            case 7:
                $this->error = '文件写入失败！';
                break;
            default:
                $this->error = '未知上传错误！';
        }
    }

    /**
     * 检测目录
     */
    private function _checkPath():bool
    {
        // 检测上传根目录
        if (! $this->_checkUploadPath()) {
            return false;
        }
        // 检查上传目录
        if (! $this->_checkSavePath()) {
            return false;
        }

        return true;
    }

    /**
     * 检测上传根目录
     * @param  string $path   根目录
     * @return boolean true-检测通过，false-检测失败
     */
    private function _checkUploadPath()
    {
        $path = $this->config['root_path'];
        if (! (is_dir($path) && is_writable($path))) {
            $this->error = '上传根目录不存在！请尝试手动创建';
            return false;
        }

        return true;
    }

    /**
     * 检测上传目录
     * @param  string $savepath 上传目录
     * @return boolean          检测结果，true-通过，false-失败
     */
    private function _checkSavePath()
    {
        $savepath = $this->config['save_path'];
        // 检测并创建目录
        $dir = $this->config['root_path'] . $savepath;
        if (! is_dir($dir)) {
            if (! mkdir($dir, 0777, true)) {
                $this->error = '目录'.$savepath . '创建失败！';
                return false;
            }
        }

        //检测目录是否可写 
        if (! is_writable($dir)) {
            return true;
        }

        $this->error = '上传目录 ' . $savepath . ' 不可写！';
        return false;
    }
}